<?php
/**
* object-oriented file system manipulation as nodes.
* This file belongs to a webappkit package (kit)
* @link http://webappkit.net
* @package pathnodes
* @subpackage common
* @author J.Ducastel <nospam0@ducastel.name>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*/

/**
* registers as low-level pathnode subclass
*/
pathnode::heritage('filenode',2);
/**
* generic file node.
*/
class fileNode extends pathNode {

    /**
    * detects if path applies to self class
    * @param string $path
    * @static
    * @access public
    * @return bool
    */
    function selfDetector($path) {
        return is_file($path);
    }/**/

    /**
    * read some bytes.
    * Reads all content if bytes is not provided. From offset, if provided, else current offset.
    * @param int bytes optional 1mo by default
    * 1048576 = 1Mo
    * @param int offset optional uses current offset by default
    * @access public
    * @return mixed file content. false if file is not readable
    */
    function read($bytes=1048576, $offset=NULL) {
        if (!$this->isReadable()) {
            // trigger_error('filenode::read : file'.$this->path.' is not readable',E_USER_NOTICE);
            return false;
        }
        // getting handle
        if (!$handle=&$this->handle()) {
            trigger_error('filenode::read could not read ('.$this->path.')');
            return false;
        }
        // setting offset if necessary
        $this->offset($offset);
        return fread($handle,$bytes);
    }/**/

    /**
    * write some data.
    * From offset, if provided, else current offset.
    * @param mixed $content
    * @param int offset optional. uses current offset if omitted
    * @access public
    * @return bool
    */
    function write($content, $offset=NULL) {
        // is file writable
        /*if (!$this->isWritable()) {
            trigger_error('filenode::write : file '.$this->path.' is not writable',E_USER_NOTICE);
            return false;
        }*/
        // getting handle
        if (!$handle=&$this->handle())
            return false;
        // setting offset if necessary
        $this->offset($offset);
        return fwrite($handle,$content);
    }/**/

    /**
    * erase from offset
    * @param int offset
    * @return bool
    */
    function erase($offset=NULL) {
        // getting handle
        if (!$handle=&$this->handle())
            return false;
        // setting offset if necessary
        //$oldOffset=$this->offset();
        $from=$this->offset($offset);
        // echo $from;
        $ok=ftruncate($handle, $from);
        // echo $this->offset();
        // $this->offset($offset);
        return $ok;
    }/**/

    /**
    * returns and edventually sets handle’s offset.
    * @param int offset optional sets new offset
    * @access public
    * @return int current offset
    */
    function offset($offset=NULL,$whence=NULL) {
        // getting file handle
        if (!$handle=&$this->handle())
            return false;
        // is new offset provided
        if (is_int($offset)) {
            // setting whence
            switch ($whence) {
                case SEEK_CUR: // new offset is from current position
                case SEEK_END: // new offset is from the end
                    break;
                default: // new offset is from start
                    $whence=SEEK_SET;
            }
            // trying to set new offset
            fseek($handle,$offset,$whence);
        }
        return ftell($handle);
    }/**/

    /**
    * sets offset to end of file
    * @access public
    * @return int new offset
    */
    function toEnd() {
        return $this->offset(0,SEEK_END);
    }
    /**
    * sets offset to start of file
    * @access public
    * @return int new offsset
    */
    function toStart() {
        return $this->offset(0,SEEK_SET);
    }

    /**
     * create the fiel if it does not exist
     * @access public
     * @return bool
     */
    function create() {
        if ($this->exists())
            return false;
        if (!$h=$this->handle('w'))
            return false;
        $this->close();
        return true;
        //return touch($this->absolute_path);
    }

    /**
    * destroy file if exists
    * @access public
    * @return bool
    */
    function delete() {
        $path=$this->getAbsolutePath();
        // echo "<p>deleting {$path}</p>";
        if (!$this->exists()) return false;
        // closing handle if necessary
        $this->close();
        return unlink($path);
    }

    /**
     * renames the file, keeping it in the same folder
     * @access public
     * @param string $name
     * @param bool $overwrite
     * @return bool
     */
    function rename($name,$overwrite=false) {
        if (!$this->exists())
            return false;
        // getting new absolute path
        $path=dirname($this->path).'/'.$name;
        // an existing file should not be overwritted without explicit order
        if (file_exists($path) and !$overwrite)
            return false;
        if (!rename($this->path,$path))
            return false;
        $this->setInfo($path);
        return true;
    }

    /**
    * moves file to new path
    * @param string $path the new file path. if includes a file name, file will be renamed
    * @param bool $overwrite an existing file should be overwrited ? default false
    * @access public
    * @return bool
    */
    function move($path,$overwrite=false) {
        if (file_exists($path) and !$overwrite)
            return false;
        if (!$this->exists())
            return false;
        if (!rename($this->getAbsolutePath(),$path))
            return false;
        $this->setInfo($path);
        return true;
    }

    /**
     * slugify path, so it can be safely used in urls
     * lower case
     * converts spaces to _
     * removes any non-standard char
     * @access public
     * @param $path desired new path. if null, will use current path
     * @return string new path
     */
    function slugifyPath($path=null) {
        // using param or current path ?
        $path=$path?$path:$this->path;
        $new='';
        for ($n=0; $n<strlen($path); $n++) {
            $char=$path[$n];
            // echo "<p>$char</p>"; continue;
            switch (true) {
                // converting spaces
                case $char==' ':
                    $new.='_';
                    break;
                // lowering case
                case strstr('abcdefghijklmnopqrstuvwxyz',strtolower($char)):
                    $new.=strtolower($char);
                    break;
                // keeping safe chars
                case strstr('/.-0123456789',$char):
                    $new.=$char;
                    break;
                // any other char is wiped
            }
        }
        // moving / renaming
        return $this->move($new);
    }

    /**
    * get file size in octets
    * @access public
    * @return int
    */
    function size() {
        return $this->exists()?filesize($this->path):0;
    }/**/

    /**
    * returns md5 hash of the file's content
    * @access public
    * @return string
    */
    function md5() {
        return md5_file($this->path);
    }/**/

    /**
    * is the file readable ?
    * @access public
    * @return bool
    */
    function isReadable() {
        if (!$this->exists()) return false;
        if (PHP_OS=='WINNT')
            return true;
        //clearstatcache();
        return is_readable($this->path);
    }/**/

    /**
    * tells if the file can be written
    * @access public
    * @return bool
    */
    function isWritable() {
        if (!$this->exists()) return false;
        if (PHP_OS=='WINNT')
            return true;
        return is_writable($this->path);
    }/**/

    /**
     * sends content to browser
     * @access public
     * @return bool
     */
    function printContent() {
        // reading and sending content
        while ($str=$this->read()) {
            print $str;
        }
        return true;
    }/**/

    /**
     * force download
     * @access public
     * @return bool
     */
    function download($filename=null) {
        // read access check
        if (!$this->isReadable()) {
            trigger_error("No read access for file to download {$this->path}",E_USER_WARNING);
            return false;
        }
        // sending headers
        if (!$filename)
            $filename=$this->getBaseName();
        $this->_downloadHeaders($filename);
        // sending content
        return $this->printContent();
    }

    /**
     * change permissions
     * @access public
     * @param int $mode octal mode, ex. 0755
     * @return bool
     */
    function chmod($mode) {
        return chmod ($this->getAbsolutePath(), $mode);
    }
/*------------------------------------------------------------------------------
                                                                      properties
------------------------------------------------------------------------------*/
    /**
    * @var type desc
    */
    /**
    * @var resource file's handle
    */
    var $_handle;
    /**
    * @var string handle's mode
    */
    var $mode;
    /**
    * @var int handler position, in octets since start
    */
    var $_offset=0;

    /**
    * returns file handle
    * @access public
    * @param string $mode
    * @return resource file hande
    */
    function & handle($mode=null) {
        if (!$this->_handle) {
            // defining default mode if required
            if (is_null($mode)) {
                switch (true) {
                    case $this->exists() and $this->isWritable():
                        $mode='r+';
                        break;
                    case $this->exists():
                        $mode='r';
                        break;
                    default:
                        $mode='w+';
                }
            }
            // opening in requested mode
            if ($this->_handle=fopen($this->path,$mode)) {
                $this->mode=$mode;
                // trigger_error('opened '.$this->path.' in mode '.$mode);
            } else
                trigger_error('failed to open '.$this->path.' in mode '.$mode);
            // echo wakapi::vardump($this);
        }
        return $this->_handle;
    }

    /**
     * close file handle if open
     * @access protected
     * @return bool
     */
    function close() {
        $h=&$this->handle();
        if (!fclose($h))
            return false;
        $this->_handle=null;
        $this->mode=null;
    }

    /**
    * executes a regex search on self name, returns results array
    * @param string $regex
    * @access public
    * @return array
    */
    function regexSelfName($regex) {
        if (!ereg($regex,$this->basename,$results))
            return false;
        return $results;
    }/**/

    /* force download file to client
    * /
    function sendFile($file, $path=NULL,$name=NULL) {
        // test de lecture

        // envoi des en-tetes
        $size=filesize($path.$file);
        if ($name===NULL) $name=$file;
        $this->sendHeaders($name,$size);

        // ouverture du fichier
        if (!$fd= fopen($path.$file, 'rb')) {
            trigger_error("pas d'acces en lecture sur $path$file",E_USER_WARNING);
            return false;
            }
        //fpassthru($fd);
        // cette methode de decoupage en parties permet de traiter de gros fichiers
        while(!feof($fd)) {
            $buffer = fread($fd, 2048);
            print $buffer;
        }
        fclose ($fd);
        return true;
    }

    /**
     * sends download headers
     * @access protected
     * @param string $filename
     * @param int $size file size in octets
     * @return bool
     */
    function _downloadHeaders($filename=null) {
        // is Internet Explorer ?
        if (isset($_SERVER['HTTP_USER_AGENT']) and $_SERVER['HTTP_USER_AGENT']=="MSIE")
            header("Content-type: application/force-download");
        // or a good browser ?
        else
            header("Content-type: application/octet-stream");
        // if size provided, send size header
        if ($size=$this->size())
            header("Content-Length: ".$size);
        // send filename header
        if (!$filename)
            $filename=$this->getbasename();
        header("Content-Disposition: attachment; filename=".$filename);
        return true;
    }/**/
}
?>